import { isNum, int } from './shims';
import { getTouch, innerWidth, innerHeight, offsetXYFromParent, outerWidth, outerHeight } from './domFns';

import type { Bounds, ControlPosition, DraggableData, MouseTouchEvent } from './types';
import { DraggableCoreProps } from '../DraggableCore';
import { DraggableProps, DraggableState } from '../Draggable';

export function getBoundPosition(props: DraggableProps, node: HTMLElement, x: number, y: number): [number, number] {
    // If no bounds, short-circuit and move on
    if (!props.bounds) return [x, y];

    // Clone new bounds
    let { bounds } = props;
    bounds = typeof bounds === 'string' ? bounds : cloneBounds(bounds);

    if (typeof bounds === 'string' && node) {
        const { ownerDocument } = node;
        const ownerWindow = ownerDocument.defaultView;
        let boundNode;
        if (bounds === 'parent') {
            boundNode = node.parentNode;
        } else {
            boundNode = ownerDocument.querySelector(bounds);
        }
        if (!(boundNode instanceof ownerWindow.HTMLElement)) {
            throw new Error('Bounds selector "' + bounds + '" could not find an element.');
        }
        const boundNodeEl: HTMLElement = boundNode; // for Flow, can't seem to refine correctly
        const nodeStyle = ownerWindow.getComputedStyle(node);
        const boundNodeStyle = ownerWindow.getComputedStyle(boundNodeEl);
        // Compute bounds. This is a pain with padding and offsets but this gets it exactly right.
        bounds = {
            left: -node.offsetLeft + int(boundNodeStyle.paddingLeft) + int(nodeStyle.marginLeft),
            top: -node.offsetTop + int(boundNodeStyle.paddingTop) + int(nodeStyle.marginTop),
            right: innerWidth(boundNodeEl) - outerWidth(node) - node.offsetLeft +
                int(boundNodeStyle.paddingRight) - int(nodeStyle.marginRight),
            bottom: innerHeight(boundNodeEl) - outerHeight(node) - node.offsetTop +
                int(boundNodeStyle.paddingBottom) - int(nodeStyle.marginBottom)
        };
    }

    // Keep x and y below right and bottom limits...
    if (isNum((bounds as Bounds).right)) x = Math.min(x, (bounds as Bounds).right);
    if (isNum((bounds as Bounds).bottom)) y = Math.min(y, (bounds as Bounds).bottom);

    // But above left and top limits.
    if (isNum((bounds as Bounds).left)) x = Math.max(x, (bounds as Bounds).left);
    if (isNum((bounds as Bounds).top)) y = Math.max(y, (bounds as Bounds).top);

    return [x, y];
}

export function snapToGrid(grid: [number, number], pendingX: number, pendingY: number): [number, number] {
    const x = Math.round(pendingX / grid[0]) * grid[0];
    const y = Math.round(pendingY / grid[1]) * grid[1];
    return [x, y];
}

export function canDragX(props: DraggableProps): boolean {
    const axis = props.axis ?? 'both';
    return axis === 'both' || axis === 'x';
}

export function canDragY(props: DraggableProps): boolean {
    const axis = props.axis ?? 'both';
    return axis === 'both' || axis === 'y';
}

// Get {x, y} positions from event.
export function getControlPosition(e: MouseTouchEvent, touchIdentifier: number | undefined, props: DraggableCoreProps, node: HTMLElement): ControlPosition | undefined {
    const touchObj = typeof touchIdentifier === 'number' ? getTouch(e, touchIdentifier) : null;
    if (typeof touchIdentifier === 'number' && !touchObj) return null; // not the right touch
    // User can provide an offsetParent if desired.
    const offsetParent = props.offsetParent || node.offsetParent || node.ownerDocument.body;
    return offsetXYFromParent(touchObj || e, offsetParent as HTMLElement, props.scale ?? 1);
}

// Create an data object exposed by <DraggableCore>'s events
export function createCoreData(state: any, node: HTMLElement, x: number, y: number): DraggableData {
    const isStart = !isNum(state.lastX());

    if (isStart) {
        // If this is our first move, use the x and y as last coords.
        return {
            node,
            deltaX: 0, deltaY: 0,
            lastX: x, lastY: y,
            x, y,
        };
    } else {
        // Otherwise calculate proper values.
        return {
            node,
            deltaX: x - state.lastX(), deltaY: y - state.lastY(),
            lastX: state.lastX(), lastY: state.lastY(),
            x, y,
        };
    }
}

// Create an data exposed by <Draggable>'s events
export function createDraggableData(props: DraggableProps, state: DraggableState, coreData: DraggableData): DraggableData {
    const scale = props.scale ?? 1;
    return {
        node: coreData.node,
        x: state.x + (coreData.deltaX / scale),
        y: state.y + (coreData.deltaY / scale),
        deltaX: (coreData.deltaX / scale),
        deltaY: (coreData.deltaY / scale),
        lastX: state.x,
        lastY: state.y
    };
}

// A lot faster than stringify/parse
function cloneBounds(bounds: Bounds): Bounds {
    return {
        left: bounds.left,
        top: bounds.top,
        right: bounds.right,
        bottom: bounds.bottom
    };
}